最近用 Docker
完成了 eggjs
后端项目的部署,不得不感叹,Docker
真的是太好用了。不仅能够一键安装 mysql
,省去了很多搭环境的事宜,而且可以直接把项目发布到 Docker
容器上进行测试,等项目需要正式上线时,就直接把做好的 Docker
镜像部署上去就好了,省去了很多项目部署上线的风险
Docker 是什么
Docker
是一个可以用来快速部署的轻量级虚拟技术,允许开发人员将自己的程序和运行环境一起打包,制作成一个 Docker
的 Image
(镜像),然后部署到服务器上,通过下载这个 Image
就可以将程序跑起来,省去了每次都安装各种依赖和环境的麻烦
Docker 镜像
操作系统分为内核和用户空间。对于 Linux 而言,内核启动后,会挂载 root 文件系统为其提供用户空间支持。而 Docker 镜像(Image),就相当于是一个 root 文件系统。除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)
Docker 容器
镜像(Image)和容器(Container)的关系,就像是面向对象程序设计中的 类 和 实例 一样,镜像是静态的定义,容器是镜像运行时的实体
仓库(Docker Registry)
镜像构建完成后,可以很容易的在当前宿主机上运行,但是,如果需要在其它服务器上使用这个镜像,我们就需要一个集中的存储、分发镜像的服务,Docker Registry
就是这样的服务
Docker 部署应用有什么优点
使用 Docker
容器部署应用快速方便,特别是应用较多时部署迁移等使用 Docker
会更方便。另外,在同一台服务器上不能同时运行多个 eggjs
应用,除非停止另外一个 eggjs
应用
更详细的介绍可以参考非官方 Docker 中文文档
Docker 的架构
为了后续更好的理解 Docker
命令的操作,我们先来大致理清下 Docker
的架构
上面这张图大致介绍了 Docker
的架构,中间是 host,也就是进行 Docker
操作的宿主机,宿主机上主要是运行 Docker Daemon
的核心程序,也就是负责做各种各样的操作,比如说下载 Docker
的镜像,比如说运行一个容器
那宿主机如何和 Docker Daemon
交互呢?实际上是通过在客户端用命令比如 build
、run
、put
交给 Daemon
,Daemon
来做实际的操作
右边的蓝的是互联网的 Sass 服务,叫做 registry
,Daemon
可以和 registry
交互,比如说 push
一个 Image
,拖拉一个 Image
,实际上是所有 Docker
用户共享 Docker
镜像的服务
简单来说,就是客户端和守护进程 Daemon
进行操作,把命令送给守护进程,守护进程来拖取镜像,运行容器,和远端的镜像仓库进行交互
项目部署实践
我这里已经用 eggjs
开发了一个后端项目,然后需要构建一个镜像,然后基于这个 Image
运行一个 container
。从而快速实现部署
大体流程
- 服务器安装好
Docker
- 本地应用根目录编写好
Dockerfile
文件 - 将整个应用一起上传到服务器目录下
- 使用终端连接服务器执行命令构建
Docker Image
- 基于镜像运行
container
,部署成功
具体操作如下
创建 Dockerfile
如果不知道 Dockerfile
文件怎么写,可以直接到 github 上查找 eggjs / docker,就可以看到完整的 Dockerfile 文件,直接拷贝粘贴到项目路径下即可
# 拉取要创建的新镜像的 base image(基础镜像),类似于面向对象里边的基础类
FROM node:8.11.3-alpine
# 设置时区
ENV TIME_ZONE=Asia/Shanghai
# 在容器内运行命令
RUN \
mkdir -p /usr/src/app \
&& apk add --no-cache tzdata \
&& echo "${TIME_ZONE}" > /etc/timezone \
&& ln -sf /usr/share/zoneinfo/${TIME_ZONE} /etc/localtime
# 创建 docker 工作目录
WORKDIR /usr/src/app
# 拷贝,把本机当前目录下的 package.json 拷贝到 Image 的 /usr/src/app/ 文件夹下
COPY package.json /usr/src/app/
# 使用 npm 安装 app 所需要的所有依赖
RUN npm i
RUN npm i --registry=https://registry.npm.taobao.org
# 拷贝本地的所有文件到路径中去
COPY . /usr/src/app
# 暴露端口。如果程序是一个服务器,会监听一个或多个端口,可以用 EXPOSE 来表示这个端口
EXPOSE 7001
# 给容器指定一个执行入口
CMD npm run start
注意事项
同时,我们还可以在 eggjs
的官网看到应用部署的文档,有两点需要防止踩坑的地方
- 第一点,因为我们的
Docker
已经是后台了,所以在部署的时候需要去掉 --daemon,不能运行在后台的后台 - 第二点,我们需要加上 --port=7001,因为
Docker
里有环境变量PORT
,如果不加上,会默认使用Docker
里的环境变量,而这个变量PORT
的值是随机生成的数字,所以在正式部署的时候就会开启这个随机数生成的端口,从而报错
以上修改都是在 package.json 文件中
构建 Image
Dockerfile
创建完成后,我们就可以在 Dockerfile
文件所在的目录下运行下面的 Docker
命令来构建一个 Image
docker build -t webshare-backend .
通过 -t 的参数,给它一个标签 share
,然后给出一个 .
, .
就是路径名 就是把这个路径底下的所有文件都送给 Docker Engine
让它来产生 Image
运行完最后会出现 successfully,代表构建成功
接着我们就可以通过 Docker Images
来查看是否真的生成了这个文件
的确是生成了一个新的 Image
, 打了一个 tag
是 latest
,有一个 ImageId
和大小 size
接着就可以运行这个 Image
运行镜像
docker run -d -p 7001:7001 webshare-backend
- -d 是
Demon
守护进程,代表容器会在后台运行 - -p 7001:7001 是把端口暴露出来,
p
是做端口映射的,:
右边的是程序本身的端口,:
左边是本地的host
的端口,把本机的 7001 映射到container
的7001,这样外网就能通过本机的 7001 访问我们的 web 了
返回了一个容器的 id
,这样就成功的用 Dockerfile
的方式来构建了一个自己的 Image
通过运行 docker ps
可以查看容器是否启动成功
我们还可以用 curl localhost:7001
测试一下,会输出接口查询内容
上传镜像
可以通过 docker push
命令,把自己创建的镜像上传到仓库中来共享
- 通常,一个仓库会包含同一个软件不同版本的镜像,而标签就常用于对应该软件的各个版本。我们可以通过 <仓库名>:<标签> 的格式来指定具体是这个软件哪个版本的镜像。如果不给出标签,将以
latest
作为默认标签。
docker tag webshare-backend:latest 服务器ip/webshare-backend:1.0
- 然后,登录镜像仓库
docker login -u 账号 -p 密码 服务器IP地址
后面显示 Login Succeeded
,就是登录成功了
这儿可能第一次会报
Service Unavailable
,需要去根路径的.docker
目录下的daemon.json
里添加信任
- 登录成功后,就可以上传自己的镜像到
Docker
仓库
docker push 服务器IP/目录名/webshare-backend:1.0
更新应用
- 通过
docker ps
命令列出运行的容器 docker stop xxx (CONTAINER ID )
停止运行该容器docker stop $(docker ps -a -q)
暂停所有运行的容器docker rm xxxx
删除containerdocker rmi 9a52e8ccdd7a(image id)
删除image- 提示无法删除的情况下,强制删除,
docker rmi -f imageId
- 按照上面的步骤重新构建镜像和启动容器
其他常用命令
- 查看镜像构建工程,
docker history webshare-backend
- 列出所有的容器,
docker ps -a
- 查看具体的容器日志,
docker logs xxx (CONTAINER ID )
docker cp src/. mycontainer:/target
在host
和container
之间拷贝文件
镜像分层
Dockerfile
中的每一行都产生一个新层,存在 Image
里的层是只读的(RO)
当 Image
被运行成为一个容器的时候,就会会产生一个新层,叫容器层 container layer
,是可读可写的(RW)
分层的好处:如果有很多的容器和 Image
,比如 A Image
有 10 层, B Image
有 7 层,他们之间可能有 5 层是共享的,那么无形之中,存储压力就会小很多
Volume( 数据卷)
提供独立于容器之外的持久化存储
因为在容器中的改动是不会被保存的,Volume
提供了比较方便的、可以持久化存储的一个技巧,比如说运行一个数据库容器,数据库的真正数据应该是被持久化的,Volume
是可以实现的,并且还可以提供给容器之间的共享数据